/*******************************************************************************
 * Copyright (c) 2010 Gregory Smith (gsmithfarmer@gmail.com).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Gregory Smith (gsmithfarmer@gmail.com) - initial API and implementation
 ******************************************************************************/
/**
 * 
 */
package com.aslan.parser.infix;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;

/**
 * This class contains the built-in functions provided automatically by the
 * {@link PostfixEvaluator} class.
 * 
 * <P>
 * Note that all function definition classes are defined as package private rather than private.
 * The reason is so they will show up the in the javadoc generated by default.
 * 
 * @author snort
 *
 */
class PostfixEvaluatorFunctions {

	private static PostfixEvaluatorFunctions INSTANCE;
	private ArrayList<IFunctionDef> allFunctions = new ArrayList<IFunctionDef>();
	
	/**
	 * Wrap the boiler plate part of a function definition is a common abstract class.
	 * h
	 * @author snort
	 *
	 */
	abstract class Template implements IFunctionDef {

		private String name;
		private int minArgs;
		private int maxArgs;
		
		Template( String name, int minArgs, int maxArgs ) {
			this.name = name;
			this.minArgs = minArgs;
			this.maxArgs = maxArgs;
		}

		Template( String name, int minArgs) {
			this(name, minArgs, minArgs );
		}
		public int getMaxArgs() { return maxArgs; }
		public int getMinArgs() { return minArgs; }
		public String getName() { return name; }
	}
	
	/**
	 * Create the single instance of this class and initialize the list of available functions.
	 *
	 */
	private PostfixEvaluatorFunctions() {
		
		for( IFunctionDef ff : new IFunctionDef[] {
				LENGTH,
				ABS,
				CEIL,
				FLOOR,
				MIN,
				MAX,
				RGB,
				toHEX,
				RANDOM,
				SUBSTRING,
				IF,
			} ) {
			allFunctions.add(ff);
		}
	}
	
	/**
	 * Determine if the value of a token should be treated as True.
	 * 
	 * @param token
	 * @return true or false.
	 */
	private boolean isTrue( IValueToken token ) {
		if( token instanceof IBooleanToken ) {
			return ((IBooleanToken)token).getInt()==IBooleanToken.TRUE;
		}
		
		if( token instanceof INumberToken ) {
			return IBooleanToken.FALSE != ((INumberToken)token).getInt();
		}
		
		String value = token.getValue();
		
		return (value==null || value.trim().length()==0)?false:true;
	}
	/**
	 * Return the single instance of this class.
	 * 
	 * @return this class.
	 */
	public static PostfixEvaluatorFunctions getInstance() {
		if( INSTANCE == null ) {
			INSTANCE = new PostfixEvaluatorFunctions();
		}
		return INSTANCE;
	}
	
	/**
	 * Return a collection of all functions defined by this class.
	 * 
	 * The collection of functions returned is the same collection that is automatically
	 * installed by {@link PostfixEvaluator}.
	 * 
	 * @return all functions defined in this class.
	 */
	public Collection<IFunctionDef> getFunctions() {
		return allFunctions;
	}
	
	/**
	 * Find the length (in characters) of an argument.
	 * 
	 */
	Template LENGTH = new Template( "LENGTH", 1) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			return new NumberToken( argList[0].getValue().length());
		}
	};
	
	/**
	 * Find the absolute value of a numeric argument (See Math.abs()).
	 * 
	 */
	Template ABS = new Template( "ABS", 1) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			if( argList[0] instanceof INumberToken ) {
				return new NumberToken( Math.abs( ((INumberToken)argList[0]).getDouble()));
			} else {
				throw new Exception("'" + argList[0].getValue() + "' passed to " + getName());
			}
		}
	};
	
	/**
	 * Find the ceiling of a numeric argument (See Math.ceil()).
	 * 
	 */
	Template CEIL = new Template( "CEIL", 1) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			if( argList[0] instanceof INumberToken ) {
				return new NumberToken( Math.ceil( ((INumberToken)argList[0]).getDouble()));
			} else {
				throw new Exception("'" + argList[0].getValue() + "' passed to " + getName());
			}
		}
	};
	
	/**
	 * Find the floor of a numeric argument (See Math.floor()).
	 * 
	 */
	Template FLOOR = new Template( "FLOOR", 1) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			if( argList[0] instanceof INumberToken ) {
				return new NumberToken( Math.floor( ((INumberToken)argList[0]).getDouble()));
			} else {
				throw new Exception("'" + argList[0].getValue() + "' passed to " + getName());
			}
		}
	};
	
	/**
	 * Find the argument with the minimum value. If all arguments are numbers, then the comparison
	 * will be numeric (e.g. Math.max()). If any value is non-numer, then the comparison will be as
	 * strings (e.g. String.equals()).
	 */
	Template MIN = new Template( "MIN", 1, Integer.MAX_VALUE) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			boolean allNumbers = true;
			for( IValueToken token : argList ) {
				allNumbers = (token instanceof INumberToken);
				if( !allNumbers) { break; }
			}
			
			if( allNumbers ) {
				double answer = ((INumberToken)argList[0]).getDouble();
				for( IValueToken token : argList ) {
					double value = ((INumberToken)token).getDouble();
					answer = Math.min(answer, value);
				}
				return new NumberToken( answer );
			} else { // Compare by strings
				String answer = argList[0].getValue();
				for( IValueToken token : argList ) {
					if( answer.compareTo(token.getValue()) > 0 ) {
						answer = token.getValue();
					}
				}
				return new StringToken(answer);
			}
		}
	};
	/**
	 * Find the argument with the maximum value. If all arguments are numbers, then the comparison
	 * will be numeric (e.g. Math.max()). If any value is non-numer, then the comparison will be as
	 * strings (e.g. String.equals()).
	 */
	Template MAX = new Template( "MAX", 1, Integer.MAX_VALUE) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			boolean allNumbers = true;
			for( IValueToken token : argList ) {
				allNumbers = (token instanceof INumberToken);
				if( !allNumbers) { break; }
			}
			
			if( allNumbers ) { // Compare as numbers
				double answer = ((INumberToken)argList[0]).getDouble();
				for( IValueToken token : argList ) {
					double value = ((INumberToken)token).getDouble();
					answer = Math.max(answer, value);
				}
				return new NumberToken( answer );
			} else { // Compare as strings
				String answer = argList[0].getValue();
				for( IValueToken token : argList ) {
					if( answer.compareTo(token.getValue()) < 0 ) {
						answer = token.getValue();
					}
				}
				return new StringToken(answer);
			}
		}
	};
	
	/**
	 * Convert Red/Green/Blue values into a single color value where:
	 * <UL>
	 * <LI>bits 16-23 represent the RED component (0-255)
	 * <LI>bits 8-15 represent the GREEN component (0-255)
	 * <LI>bits 0-7 represent the BLUE component (0-255)
	 * </UL>
	 * <P>
	 * Examples:
	 * <UL>
	 * <LI>RGB( 255,0,0 ) // pure RED
	 * <LI>RGB( 0,0,255) // pure BLUE
	 * </UL>
	 * <P>
	 * If a number out of range is specifed, it will be rounded to 0 or 255.
	 */
	Template RGB = new Template( "RGB", 3) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			int[] rgb = new int[3];
			
			for( int n = 0; n < rgb.length; n++ ) {
				if( argList[n] instanceof INumberToken ) {
					rgb[n] = ((INumberToken)argList[n]).getInt();
					if( rgb[n] < 0 ) { rgb[n] = 0; }
					if( rgb[n] > 255 ) { rgb[n] = 255; }
				} else {
					throw new Exception("Non-numeric value '" + argList[n].getValue() + "' passed to " + getName());
				}
			}
			
			return new NumberToken(rgb[0]<<16 | rgb[1]<<8 | rgb[2]);
		}
	};
	
	/**
	 * Convert a number to a hex representation.
	 */
	Template toHEX = new Template( "toHex", 1) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			if( argList[0] instanceof INumberToken ) {
				int ival = ((INumberToken)argList[0]).getInt();
				return new StringToken(Integer.toHexString(ival));
			} else {
				throw new Exception("Non-numeric value '" + argList[0].getValue() + "' passed to " + getName());
			}
		}
	};
	
	private static Random random = null;
	/**
	 * Return a random number between 0.0 and 1.0.
	 * If an integer argument is supplied, it will be used to seed the random number generator.
	 */
	Template RANDOM = new Template( "RANDOM", 0, 1) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			
			if( random == null ) {
				random = new Random();
			}
			
			if( argList.length == 1 && argList[0] instanceof INumberToken ) {
				random.setSeed( ((INumberToken)argList[0]).getInt());
			}
			
			return new NumberToken( random.nextDouble());
		}
	};
	
	/**
	 * Return the substring of a string.
	 * <blockquote>
	 * <pre>
	 * SUBSTRING( "string", startIndex [, endIndex] )
	 * </pre>
	 * </blockquote>
	 * <P>
	 * Note that index values are zero based (like C or Java).
	 * <P>
	 * Except for the fact that this method will deal with out-of-bounds start and end index values,
	 * this function works exactly like the java String.substring() method.
	 */
	Template SUBSTRING = new Template( "SUBSTRING", 2, 3) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			
			if( !(argList[1] instanceof INumberToken)) {
				throw new Exception("Non-numeric beginning index '" + argList[1].getValue() + "' passed to " + getName());
			}
			
			if( argList.length > 2 && !(argList[2] instanceof INumberToken)) {
				throw new Exception("Non-numeric end index '" + argList[1].getValue() + "' passed to " + getName());
			}
			
			String value = argList[0].getValue();
			int startIndex = ((INumberToken)argList[1]).getInt();
			int endIndex = argList.length>2?((INumberToken)argList[2]).getInt():value.length();
			
			if( startIndex < 0 ) { startIndex = 0; }
			if( endIndex > value.length() ) { endIndex = value.length(); }
			if( endIndex < 0 || startIndex >= value.length() || endIndex <= startIndex) {
				return new StringToken("");
			}
			
			return new StringToken( value.substring( startIndex, endIndex ));
		}
	};
	
	/**
	 * Return a value depending on the true/false value of another expression.
	 * <blockquote>
	 * <pre>
	 * IF( booleanExpression, trueResult [, falseResult] )
	 * </pre>
	 * </blockquote>
	 */
	Template IF = new Template( "IF", 2, 3) {

		public IValueToken eval(IValueToken[] argList) throws Exception {
			
			if( isTrue( argList[0])) {
				return new StringToken( argList[1].getValue());
			} else {
				return new StringToken( argList.length==2?"":argList[2].getValue());
			}
		}
	};
}
